type Uiconfig = {
  perBoxSize: number, // 每个小方块大小
  gap: number, // 方块间隙
  borderRadius: number, // 大盒子圆角
  backgroundColor: string, // 大盒子背景
  backgroundBoxColor: string // 小方块背景
}

class BoxUtil {
  private static nums = {
    "2": {
        "color": "#776e65",
        "backgroundColor": "#eee4da",
        "fontSize": 65
    },
    "4": {
        "color": "#776e65",
        "backgroundColor": "#ede0c8",
        "fontSize": 65
    },
    "8": {
        "color": "#f9f6f2",
        "backgroundColor": "#f2b179",
        "fontSize": 55
    },
    "16": {
        "color": "#f9f6f2",
        "backgroundColor": "#f59563",
        "fontSize": 55
    },
    "32": {
        "color": "#f9f6f2",
        "backgroundColor": "#f67c5f",
        "fontSize": 55
    },
    "64": {
        "color": "#f9f6f2",
        "backgroundColor": "#f65e3b",
        "fontSize": 55
    },
    "128": {
        "color": "#f9f6f2",
        "backgroundColor": "#edcf72",
        "fontSize": 45
    },
    "256": {
        "color": "#f9f6f2",
        "backgroundColor": "#edcc61",
        "fontSize": 45
    },
    "512": {
        "color": "#f9f6f2",
        "backgroundColor": "#edc850",
        "fontSize": 45
    },
    "1024": {
        "color": "#f9f6f2",
        "backgroundColor": "#abe358",
        "fontSize": 35
    },
    "2048": {
        "color": "#f9f6f2",
        "backgroundColor": "#4dd9cf",
        "fontSize": 35
    },
    "4096": {
        "color": "#f9f6f2",
        "backgroundColor": "#a283f9",
        "fontSize": 35
    },
    "8192": {
        "color": "#f9f6f2",
        "backgroundColor": "#f98383",
        "fontSize": 35
    }
}

  static createNumBox(
    num: number,
    col: number,
    row: number,
    size: number,
    borderRadius: number,
    gap: number
  ) {
    const numInfo = this.nums["" + num]
    return $(`
      <div class="numBox" style="position: absolute;
      left: ${gap + (gap + size) * col}px; top: ${gap + (gap + size) * row}px;
      width: ${size}px; height: ${size}px; line-height: ${size}px;
      border-radius: ${borderRadius}px;
      background-color: ${numInfo.backgroundColor};
      font-size: ${numInfo.fontSize}px; color: ${numInfo.color}; text-align: center;
      z-index: 1;">${num}</div>
    `)
  }

  static createBackgroundBox(
    left: number,
    top: number,
    size: number,
    borderRadius: number,
    color: string
  ) {
    return $(`
      <div style="position: absolute;
      left: ${left}px; top: ${top}px;
      width: ${size}px; height: ${size}px;
      border-radius: ${borderRadius}px;
      background-color: ${color};
      z-index: 0;"></div>
    `)
  }
}

class NumBox {
  private container: JQuery<HTMLElement>
  num: number
  box: JQuery<HTMLElement>
  col: number
  row: number
  private size: number
  private borderRadius: number
  private gap: number

  isMerged: false

  constructor(
    container: JQuery<HTMLElement>,
    num: number,
    col: number,
    row: number,
    size: number,
    borderRadius: number,
    gap: number
  ) {
    this.container = container
    this.num = num
    this.col = col
    this.row = row
    this.size = size
    this.borderRadius = borderRadius
    this.gap = gap
    this.refresh()
  }

  private refresh() {
    if(this.box)
      this.box.remove()
    this.box = BoxUtil.createNumBox(this.num, this.col, this.row, this.size, this.borderRadius, this.gap)
    this.box.appendTo(this.container)
  }

  // 滑动动画
  async moveTo(newCol: number, newRow: number) {
    const xDis = (newCol - this.col) * (this.size + this.gap)
    const yDis = (newRow - this.row) * (this.size + this.gap)
    if(xDis + yDis !== 0) {
      this.col = newCol
      this.row = newRow
      return new Promise<void>(resolve => {
        const moveTime = (Math.abs(xDis + yDis) / (this.size + this.gap)) * 80
        this.box.animate({
          left: (this.box[0].offsetLeft + xDis) + "px",
          top: (this.box[0].offsetTop + yDis) + "px"
        }, moveTime, "easeInOutCubic", resolve)
      })
    }
  }

  private destroy() {
    this.box.fadeOut(() => {
      this.box.remove()
    })
  }

  // 数字变化
  async mergeTo(otherBox: NumBox) {
    this.box.css("z-index", 2)
    await this.moveTo(otherBox.col, otherBox.row)
    otherBox.num *= 2
    otherBox.refresh()
    this.destroy()
  }
}

class Game2048 {
  private uiConfig: Uiconfig
  private container: JQuery<HTMLElement>
  private scoreSpan: JQuery<HTMLElement>
  private bestSpan: JQuery<HTMLElement>
  private mainPanel: JQuery<HTMLElement>
  private numBoxs: NumBox[] = new Array(16).fill(null)
  private score: number
  private isMerging: boolean = false
  private isGameOver: boolean = false

  constructor(container: string, config: Uiconfig = {
    perBoxSize: 100,
    gap: 4,
    borderRadius: 4,
    backgroundColor: "#f98995",
    backgroundBoxColor: "rgba(238,228,218,0.35)"
  }) {
    this.container = $(container)
    this.uiConfig = config
    this.initUi()
    this.bindKeys()
    this.refreshPage()
  }

  private initUi() {
    const width = this.uiConfig.perBoxSize * 4 + this.uiConfig.gap * 5
    this.container.css("width", width)

    // 计分、按钮面板
    const uiPanel = $(`
      <div class="uiPanel">
        <div class="gameName">2048<span style="font-size:12px;">&nbsp;&nbsp;&nbsp;Use arrow keys</span><div class="tip">Join numbers to get to the 2048 tile!</div></div>
        <div class="btn">
          <div style="display:flex;justify-content:space-between;">
            <div class="score">0</div>
            <div class="best">0</div>
          </div>
          <div class="newGameBtn">New Game</div>
        </div>
      </div>
    `)
    this.container.append(uiPanel)
    this.scoreSpan = uiPanel.find(".score")
    this.bestSpan = uiPanel.find(".best")
    uiPanel.find(".newGameBtn").on("click", event => this.newGame())

    // 游戏主面板
    this.mainPanel = $(`
      <div class="mainPanel"
        style="width: ${width}px; height: ${width}px;
        background-color: ${this.uiConfig.backgroundColor};
        border-radius: ${this.uiConfig.borderRadius}px;"></div>
    `)
    this.container.append(this.mainPanel)

    // 16个小方块
    for (let i = 0; i < 16; i++) {
      this.mainPanel.append(BoxUtil.createBackgroundBox(
        (this.uiConfig.perBoxSize * (i % 4) + this.uiConfig.gap * (i % 4 + 1)),
        (this.uiConfig.perBoxSize * Math.floor(i / 4) + this.uiConfig.gap * (Math.floor( i / 4) + 1)),
        this.uiConfig.perBoxSize,
        this.uiConfig.borderRadius,
        this.uiConfig.backgroundBoxColor
      ))
    }

    // const numBox = new NumBox(this.mainPanel, 2, 0, 0, this.uiConfig.perBoxSize, this.uiConfig.borderRadius, this.uiConfig.gap)
    // numBox.moveTo(0, 3)
    // const numBox1 = new NumBox(this.mainPanel, 2, 0, 3, this.uiConfig.perBoxSize, this.uiConfig.borderRadius, this.uiConfig.gap)
    // numBox.mergeTo(numBox1)
  }

  // 键盘绑定
  private bindKeys() {
    document.addEventListener("keyup", event => {
      switch (event.code) {
        case "ArrowLeft": 
          return this.merge([0, 4, 8, 12], 1)
        case "ArrowRight":
          return this.merge([3, 7, 11, 15], -1)
        case "ArrowUp": 
          return this.merge([0, 1, 2, 3], 4)
        case "ArrowDown":
          return this.merge([12, 13, 14, 15], -4)
        default:
          return null
      }
    })
  }

  // 重置游戏
  private newGame() {
    this.mainPanel.find(".numBox").remove()
    this.numBoxs.fill(null)
    this.score = 0
    this.isMerging = false
    this.isGameOver = false
    localStorage.score = "0"

    this.addNewNumBox(2)
  }

  // 刷新页面
  private refreshPage() {
    if (!localStorage.score) {
      localStorage.score = 0
    }
    this.score = Number(localStorage.score)
    this.scoreSpan.text(localStorage.score)
    this.bestSpan.text(localStorage.best)
    if (localStorage.numBoxs) {
      JSON.parse(localStorage.numBoxs).map((item, index) => {
        if (item) {
          const numBox = new NumBox(this.mainPanel, item.num, item.col, item.row, item.size, item.borderRadius, item.gap)
          this.numBoxs[index] = numBox
        }
      })
    } else {
      this.addNewNumBox(2)
    }
  }

  // 随机添加numBox
  private addNewNumBox(size: number) {
    const emptyPosArr = this.getRandomEmptyGrids(size)
    for(let index of emptyPosArr) {
      const num = Math.random() < 0.8 ? 2 : 4
      this.numBoxs[index] = new NumBox(this.mainPanel, num, index % 4, Math.floor(index / 4), this.uiConfig.perBoxSize, this.uiConfig.borderRadius,this.uiConfig.gap)
    }
    this.scoreSpan.text(localStorage.score)
    this.bestSpan.text(localStorage.best)
    localStorage.numBoxs = JSON.stringify(this.numBoxs)
  }

  // 获取随机空余位置
  private getRandomEmptyGrids(size: number): Array<number> {
    const emptyPosArr = []
    this.numBoxs.forEach((item, index) => {
      if(!item) {
        emptyPosArr.push(index)
      }
    })
    const result = []
    while(result.length < size && emptyPosArr.length > 0) {
      let randomI = Math.floor(Math.random() * emptyPosArr.length)
      result.push(emptyPosArr.splice(randomI, 1)[0])
    }
    return result
  }

  // down [12, 13, 14, 15] -4
  // up [0, 1, 2, 3] 4
  // right [3, 7, 11, 15] -1
  // left [0, 4, 8, 12] 1
  // startIndexs 最终位置 nextDelta 路径步长
  private async merge(startIndexs: number[], nextDelta: number) {
    if (this.isGameOver || this.isMerging) {
      return
    }
    this.isMerging = true
    const promises = [] // 记录异步动画
    let addNew = false // 判断是否添加新方块
    this.numBoxs.forEach(item => item && (item.isMerged = false)) // 重置合并标记
    let isMerge = false

    for(let startIndex of startIndexs) {
      for (let i = 1; i < 4; i++) {
        const curIndex = startIndex + i * nextDelta
        const curBox = this.numBoxs[curIndex]
        if (curBox) {
          addNew = true
          const reachableBox = this.findReachableBox(curIndex, -nextDelta, startIndex)
          if (reachableBox) { // 需要改变
            isMerge = true
            this.numBoxs[curIndex] = null
            if (reachableBox.box) { // 合并
              reachableBox.box.isMerged = true
              promises.push(curBox.mergeTo(reachableBox.box))
              // 得分
              this.score += reachableBox.box.num * 2
              localStorage.score = String(this.score)
              // 最佳
              localStorage.best = this.score > (localStorage.best || 0) ? this.score : localStorage.best
            } else { // 移动
              this.numBoxs[reachableBox.index] = curBox
              promises.push(curBox.moveTo(reachableBox.index % 4, Math.floor(reachableBox.index / 4)))
            }
          }
        }
      }
    }
    await Promise.all(promises)
    
    if (!this.isGameOver && isMerge && addNew) {
      this.addNewNumBox(1)
    }
    if (this.isGameOver = !!this.checkGameOver()) {
      this.showComfirmPanel()
      localStorage.score = "0"
      localStorage.removeItem("numBoxs")
    }
    this.isMerging = false
  }

  private findReachableBox(curIndex: number, nextDelta: number, endIndex: number) {
    let reachableInfo = null
    const curNumBox = this.numBoxs[curIndex]
    for (let i = 1; i < 4; i++) {
      let otherIndex = curIndex + nextDelta * i
      const otherBox = this.numBoxs[otherIndex]
      if (!otherBox) {
        reachableInfo = {
          index: otherIndex,
          box: null
        }
      } else if (!otherBox.isMerged && curNumBox.num === otherBox.num) {
        reachableInfo = {
          index: otherBox,
          box: otherBox
        }
      } else {
        break
      }
      if (otherIndex === endIndex) {
        break
      }
    }
    return reachableInfo
  }

  // 判断游戏是否结束
  private checkGameOver() {
    // 16格是否铺满
    const isFull = this.numBoxs.every(item => item)
    if (isFull) { // 判断是否可以合并
      for (let i = 0; i < 16; i++) {
        const curBox = this.numBoxs[i]
        if (i % 4 < 3) { // 前三列
          const rightNumBox = this.numBoxs[i + 1]
          if (rightNumBox.num === curBox.num) {
            return false
          }
        }
        if (i / 4 < 3) { // 上三行
          const downNumBox = this.numBoxs[i + 4]
          if (downNumBox.num === curBox.num) {
            return false
          }
        }
      }
      return 'Game Over ~~'
    }
    // 2048
    const is2048 = this.numBoxs.some(item => item && (item.num === 2048))
    if (is2048) {
      return '2 0 4 8 !'
    }
  }

  private showComfirmPanel() {
    // 确认框
    const comfirmPanel = $(`
      <div class="comfirmPanel">
        <div style="font-size:30px;border-bottom:1px solid #eee;height:80px;line-height:80px;">${this.checkGameOver()}</div>
        <div style="display:flex;cursor:pointer;">
          <div class="playAgainBtn1">取消</div>
          <div class="playAgainBtn" style="width:50%;">再玩一次</div>
        </div>
      </div>
    `)
    this.container.append(comfirmPanel)
    comfirmPanel.find(".playAgainBtn1").on("click", event => this.container.find(".comfirmPanel").remove())
    comfirmPanel.find(".playAgainBtn").on("click", event => {
      this.newGame()
      this.container.find(".comfirmPanel").remove()
    })
  }
}